Tool

abstract class Tool<TInput, TOutput, TContext>(val name: String, val description: String, val inputSerializer: KSerializer<TInput>, val outputSerializer: KSerializer<TOutput>, val strict: Boolean = true, val inputExamples: List<String> = emptyList(), val metadata: Map<String, JsonElement> = emptyMap(), val providerExecuted: Boolean = false, val providerOptions: Map<String, JsonElement> = emptyMap())

A tool the LLM can call. There are two idiomatic ways to define one:

1. Extend it — for a reusable, dependency-injected tool that deserves a name (mirrors how a concrete agent extends ToolLoopAgent):

class SearchDocsTool(private val repo: DocRepository) :
Tool<SearchInput, List<SearchResult>, AppContext>(
name = "searchDocs",
description = "Search the product documentation",
inputSerializer = serializer(),
outputSerializer = serializer(),
) {
override suspend fun ToolExecutionContext<AppContext>.execute(input: SearchInput) =
repo.search(input.query)
}
// usage: toolSetOf(SearchDocsTool(repo)) — one class, many instances

Override only the hooks you need (needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable); they no-op by default. For a tool that emits preliminary snapshots before its final value, extend StreamingTool and override StreamingTool.executeStream instead.

2. Use the tool / streamingTool factories — for trivial inline tools where a named class is overkill. The factories build an anonymous subclass.

Per invariant I-2, tools use inputSchema (v6) — never parameters (v5, removed). Per I-8 tools are stateless: inputs in, outputs out, no shared mutable state — so a single instance is safely reused across agents.

Type Parameters

TInput

Application-defined input type, must be @Serializable. The model produces JSON matching its schema; the agent deserializes via inputSerializer before calling execute.

TOutput

Application-defined output type, must be @Serializable. Serialized into the model's tool-result message via outputSerializer, or summarized via toModelOutput if the raw output would be too verbose for the model.

TContext

Caller-supplied typed context propagated through the whole loop (set via callOptionsSchema, accessed inside execute as this.context).

Internally the agent loop always drives a Flow (the v6 alignment per tool.ts:68-71's AsyncIterable<OUTPUT> shape): a plain Tool wraps its single execute value in a one-emission flow; a StreamingTool passes its Flow straight through. The loop collects with one-step lookahead — non-final emissions become StreamEvent.ToolResult(preliminary = true), the last emission is the final result that lands in the model's message log.

Inheritors

Constructors

Link copied to clipboard
constructor(name: String, description: String, inputSerializer: KSerializer<TInput>, outputSerializer: KSerializer<TOutput>, strict: Boolean = true, inputExamples: List<String> = emptyList(), metadata: Map<String, JsonElement> = emptyMap(), providerExecuted: Boolean = false, providerOptions: Map<String, JsonElement> = emptyMap())

Properties

Link copied to clipboard
Link copied to clipboard

Few-shot example inputs encoded as JSON strings. Mirrors v6's tool.inputExamples (per historical parity gap #19). Disproportionate quality lift on small models — Gemma 4 E2B's tool-call accuracy notably improves with 2-3 in-tool examples. ToolSet.descriptors inlines these into the description as Example: <json> so every provider sees them (most don't expose a separate examples field).

Link copied to clipboard
val inputSerializer: KSerializer<TInput>
Link copied to clipboard
val metadata: Map<String, JsonElement>

Application-defined metadata bag. Mirrors v6's tool.metadata (per historical parity gap #34). Opaque to the loop — consumers (logger middleware, telemetry, host-side gating) can read it via ToolSet.byName. Examples: feature-flag key, billing tier, owning team. Empty by default.

Link copied to clipboard
Link copied to clipboard
val outputSerializer: KSerializer<TOutput>
Link copied to clipboard
Link copied to clipboard
val providerOptions: Map<String, JsonElement>

Provider-specific config for this tool, sent to the model on the wire (e.g. Anthropic cache_control). Distinct from metadata, which is host-side only. Mirrors v6's tool.providerOptions. Empty by default.

Link copied to clipboard

Per best practice #9: strict JSON schema enforcement is opt-in per tool, not global. Some providers reject schemas with format/regex constraints under strict mode; mark such tools strict = false.

Functions

Link copied to clipboard
abstract suspend fun ToolExecutionContext<TContext>.execute(input: TInput): TOutput

Run the tool — the common single-value case. The model's JSON input has already been deserialized into TInput; return the typed output. this is the ToolExecutionContext, so the typed context, abortSignal, toolCallId, stepNumber, messages, and writer are in scope.

Link copied to clipboard
open suspend fun needsApproval(input: TInput, options: ToolPredicateOptions<TContext>): Boolean

Approval gate (parity gap #17). Return true to require host approval before execute runs: the loop ends with the call surfaced in GenerateResult.pendingApprovals until the host resumes. The options carry toolCallId + messages + experimental_context, so the gate can decide from conversation history or call identity. Default: never gates.

Link copied to clipboard
open suspend fun onInputAvailable(toolCallId: String, input: TInput)

Lifecycle hook — fired after the streamed JSON parses into the typed TInput, just before execute runs. Mirrors v6's tool.onInputAvailable. No-op by default.

Link copied to clipboard
open suspend fun onInputDelta(streamingId: String, delta: String)

Lifecycle hook (gap #18) — fired as the input JSON streams in token-by-token (StreamEvent.ToolInputDelta). The accumulated input isn't yet valid JSON; this is for raw-character pre-warm. No-op by default.

Link copied to clipboard
open suspend fun onInputStart(streamingId: String)

Lifecycle hook (gap #18) — fired when the model commits to calling this tool (StreamEvent.ToolInputStart). Pre-warm here: UI spinner, cache priming, etc. No-op by default.

Link copied to clipboard

Model-visible summary (parity gaps #17 + #14). Return a structured ToolResultOutput (Text / Json / Error / …) to send the model a short summary in place of the full (possibly verbose) output; the loop bridges it to ContentPart.ToolResult.modelVisible (+ isError). Return null (default) to send the raw output as-is.